﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Threading;

namespace Gmantis.Uploader
{
    public interface IUploadManager
    {
        // Events
        event EventHandler<UploadCompletedEventArgs> UploadCompleted;
        event EventHandler<UploadProgressChangedEventArgs> UploadProgressChanged;

        // Methods
        bool BeginUpload(UploadManagerSettings settings, UploadFilePartPackage package, Dictionary<HttpRequestHeader, string> headers, Dictionary<string, string> parameters);
        void Cancel();
    }

    public abstract class Uploader
    {
        // Fields
        private long _bytesSent;
        private SynchronizationContext _context = SynchronizationContext.Current;
        private List<UploadFileInfo> _files;
        private List<UploadFileInfo> _filesToUpload;
        private Dictionary<HttpRequestHeader, string> _headers = new Dictionary<HttpRequestHeader, string>();
        private bool _isBusy;
        internal IUploadManager _manager;
        private long _maximumUploadSize;
        private Dictionary<string, string> _parameters = new Dictionary<string, string>();
        private IEnumerator<UploadFilePartPackage> _pendingPartPackages;
        private List<string> _responses;
        private UploadManagerSettings _settings;
        private FilePackager _splitterAlgorithm;
        private long _totalBytesToUpload;
        private object _userState;

        // Events
        public event EventHandler<UploadCompletedEventArgs> UploadCompleted;
        public event EventHandler<UploadProgressChangedEventArgs> UploadProgressChanged;

        // Methods
        public Uploader(UploadManagerSettings settings, FilePackager filePackager, IUploadManager uploadManager)
        {
            if (uploadManager == null)
                throw new ArgumentNullException("uploadAlgorithm");

            this._settings = settings;
            this._files = new List<UploadFileInfo>();
            this._maximumUploadSize = -1L;
            this._splitterAlgorithm = filePackager;
            this._manager = uploadManager;
            this._manager.UploadProgressChanged += new EventHandler<UploadProgressChangedEventArgs>(this._algorithm_UploadProgressChanged);
            this._manager.UploadCompleted += new EventHandler<UploadCompletedEventArgs>(this._algorithm_UploadCompleted);
        }

        private void _algorithm_UploadCompleted(object sender, UploadCompletedEventArgs e)
        {
            if (this._filesToUpload != null)
            {
                if (e.Success)
                {
                    this._responses.Add(e.Response as string);
                    this._bytesSent += this._pendingPartPackages.Current.GetSize();
                    this.OnUploadProgressChanged(this._bytesSent, this._totalBytesToUpload);
                    if (!this.UploadNextFile())
                    {
                        this.OnUploadCompleted(e.Success, e.Cancelled, e.Error, this._responses.ToArray());
                    }
                }
                else
                {
                    this.OnUploadCompleted(e.Success, e.Cancelled, e.Error, this._responses.ToArray());
                }
            }
        }

        private void _algorithm_UploadProgressChanged(object sender, UploadProgressChangedEventArgs e)
        {
            this.OnUploadProgressChanged(this._bytesSent + e.BytesUploaded, this._totalBytesToUpload);
        }

        internal void AddFile(UploadFileInfo file)
        {
            this._files.Add(file);
        }

        public void AddFile(FileInfo file)
        {
            this.AddFile(new UploadFileInfo(file));
        }

        public void AddFile(string fileName)
        {
            this.AddFile(new UploadFileInfo(fileName));
        }

        public void AddFile(string name, Stream file)
        {
            this.AddFile(new UploadFileInfo(name, file));
        }

        public void AddFiles(IEnumerable<FileInfo> files)
        {
            foreach (FileInfo info in files)
            {
                this.AddFile(info);
            }
        }

        public bool BeginUploadFiles()
        {
            if (this._isBusy || (this._files.Count <= 0))
            {
                return false;
            }
            this._isBusy = true;
            try
            {
                this.PrepareFiles();
            }
            catch (Exception exception)
            {
                this.CloseFiles();
                throw new Exception("There was an error trying to open the files", exception);
            }
            if ((this._maximumUploadSize != -1L) && (this._maximumUploadSize < this._splitterAlgorithm.GetMaxSizePerUpload()))
            {
                throw new MaximumUploadSizeExceededException();
            }
            this.UploadPackagesOneByOne(this._splitterAlgorithm.GetPackages());
            return true;
        }

        public bool Cancel()
        {
            if (this._isBusy)
            {
                this._manager.Cancel();
                this.ReleaseResources();
                return true;
            }
            return false;
        }

        private void CloseFiles()
        {
            foreach (UploadFileInfo info in this._filesToUpload)
            {
                info.Close();
            }
        }

        private void OnUploadCompleted(bool success, bool cancelled, Exception exc, object response)
        {
            if (this.UploadCompleted != null)
            {
                List<string> files = new List<string>();
                foreach (UploadFileInfo info in this._filesToUpload)
                {
                    files.Add(info.Name);
                }
                this._context.Post(sender => this.UploadCompleted(this, new UploadCompletedEventArgs(success, cancelled, exc, files.ToArray(), response, this._userState)), null);
            }
            this.ReleaseResources();
            this._isBusy = false;
        }

        private void OnUploadProgressChanged(long filesUploaded, long totalFilesToUpload)
        {
            SendOrPostCallback d = null;
            if (this.UploadProgressChanged != null)
            {
                if (d == null)
                {
                    d = sender => this.UploadProgressChanged(this, new UploadProgressChangedEventArgs(filesUploaded, totalFilesToUpload, this._userState));
                }
                this._context.Post(d, null);
            }
        }

        private void PrepareFiles()
        {
            this._filesToUpload = new List<UploadFileInfo>();
            foreach (UploadFileInfo info in this._files)
            {
                this._filesToUpload.Add(info);
                info.Prepare();
            }
            this._splitterAlgorithm.Files = this._filesToUpload;
        }

        private void ReleaseResources()
        {
            this.CloseFiles();
            this._filesToUpload.Clear();
            this._pendingPartPackages = null;
            this._responses = new List<string>();
        }

        private bool UploadNextFile()
        {
            if (this._pendingPartPackages.MoveNext())
            {
                this._manager.BeginUpload(this.Settings, this._pendingPartPackages.Current, this._headers, this._parameters);
                return true;
            }
            return false;
        }

        private void UploadPackagesOneByOne(IEnumerable<UploadFilePartPackage> _partPackages)
        {
            this._pendingPartPackages = _partPackages.GetEnumerator();
            this._totalBytesToUpload = this._splitterAlgorithm.GetTotalSize();
            this._bytesSent = 0L;
            this._responses = new List<string>();
            this.UploadNextFile();
        }

        // Properties
        public List<UploadFileInfo> Files
        {
            get
            {
                return this._files;
            }
        }

        public Dictionary<HttpRequestHeader, string> Headers
        {
            get
            {
                return this._headers;
            }
        }

        public bool IsBusy
        {
            get
            {
                return this._isBusy;
            }
        }

        public long MaximumUploadSize
        {
            get
            {
                return this._maximumUploadSize;
            }
            set
            {
                this._maximumUploadSize = value;
                this._splitterAlgorithm.MaximumUploadSize = value;
            }
        }

        public Dictionary<string, string> Parameters
        {
            get
            {
                return this._parameters;
            }
        }

        public virtual UploadManagerSettings Settings
        {
            get
            {
                return this._settings;
            }
        }

        public object UserState
        {
            get
            {
                return this._userState;
            }
            set
            {
                this._userState = value;
            }
        }
    }
}
